/*
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.expression;

import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.tree.TreeBuilder;
import de.odysseus.el.tree.TreeStore;
import de.odysseus.el.tree.impl.Builder;
import de.odysseus.el.tree.impl.Cache;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import tal.drivechain.chain.Chain;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.parsing.ParsingAid;
import tal.drivechain.parsing.Tokenizer;
import tal.drivechain.variable.types.DataType;
import tal.drivechain.variable.VarTable;
import tal.drivechain.variable.Variable;

/**
 *
 * @author eisental
 */
public class dcExpression {
    private DataType returnType;
    private static ExpressionFactory factory = null;

    private dcContext context;
    private ValueExpression valExpression;

    private boolean requireApostrophes;

    public dcExpression(String expr, DataType returnType) throws IllegalArgumentException {
        this(expr, returnType, false);
    }

    public dcExpression(String expr, DataType returnType, boolean requireStringApostrophes) throws IllegalArgumentException{
        requireApostrophes = requireStringApostrophes;
        expr = expr.trim();
        if (requireStringApostrophes && returnType.matches(DataType.STRING) && expr.length()>0) {
            // must use apostrophes.
            boolean pass = true;
            if (expr.charAt(0)!='\'') pass = false;
            else if (Tokenizer.balanced(expr, 0, '\'', '\'')!=expr.length()-1) {
                pass = false;
            }

            if (!(expr.charAt(0)=='\'' && expr.charAt(expr.length()-1)=='\'')) {
                if (!expr.startsWith("${") || Tokenizer.balanced(expr, 1, '{', '}')!=expr.length()-1)
                    throw new IllegalArgumentException("Mixed string expression must be enclosed in apostrophes (\'): " + expr);

            } else expr = expr.substring(1, expr.length()-1);

            
        } 


        if (factory == null) {
            TreeBuilder builder = new Builder(Builder.Feature.METHOD_INVOCATIONS, Builder.Feature.VARARGS);
            TreeStore store = new TreeStore(builder, new Cache(1000));
            factory = new ExpressionFactoryImpl(store);
        }

        this.returnType = returnType;
        context = new dcContext();
        addFunctions();
        addConstants();
        Class returnClass;
        if (returnType!=null)
            returnClass = returnType.getValueClass();
        else returnClass = Object.class;
        try {
            valExpression = factory.createValueExpression(context, ParsingAid.parseEscapeCharacters(expr), returnClass);

        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalArgumentException("While creating expression: " + e.getMessage());
        }
    }

    public void setVariable(String name, Object value) {
        factory.createValueExpression(context, "${" + name + "}", value.getClass()).setValue(context, value);
    }

    private void addConstants() {
        context.setVariable("pi", factory.createValueExpression(Math.PI, double.class));
        context.setVariable("exp", factory.createValueExpression(Math.E, double.class));
    }

    private void addFunctions() {
        addFromClass("math", Math.class);
        addFromClass("sys", SystemFunctions.class);
        addFromClass("dc", DrivechainFunctions.class);
        addFromClass("math", ExtraMathFunctions.class);
        addFromClass("midi", MidiFunctions.class);
    }

    private void addFromClass(String namespace, Class c) {
        for (Method m : c.getMethods()) {
            if (Modifier.isStatic(m.getModifiers()))
                context.setFunction(namespace, m.getName(), m);
        }

    }

    public void setReturnDataType(DataType type) {
        this.returnType = type;
    }

    public DataType getReturnVarType() {
        return returnType;
    }

    public Object evaluate() throws IllegalArgumentException {
        return evaluate(null);
    }

    public Object evaluate(ChainNode nodeContext) throws IllegalArgumentException {
//        System.out.println("context==" + (nodeContext==null?"null":nodeContext.getPath()));
        if (nodeContext!=null) {
            String[] lookups = context.getNameLookups().toArray(new String[0]);
            for (String name : lookups) {
                boolean found = false;
                if (!(nodeContext instanceof Chain) && nodeContext.getParentChain()!=null && nodeContext.getParentChain().getVarTable()!=null) {
//                    System.out.println("searching global vars");
                    VarTable globalVars = nodeContext.getParentChain().getVarTable();
                    if (globalVars.containsKey(name)) {
                        Variable v = globalVars.get(name);
                        Object value = v.getValue(nodeContext);
                        Class valClass = value.getClass();
                        factory.createValueExpression(context, "${" + v.getName() + "}", valClass).setValue(context, value);
                        found = true;
                    }

                }

                if (!found && nodeContext.getVarTable()!=null) {
//                    System.out.println("searching local vars");
                    VarTable localVars = nodeContext.getVarTable();
                    for (String varName : localVars.keySet())
//                        System.out.println(varName + ": " + localVars.get(varName));
//                    System.out.println("do we have a match with " + name + "?");
                    if (localVars.containsKey(name)) {
//                        System.out.println("yes!!");
                        Variable v = localVars.get(name);
                        Object value = v.getValue(nodeContext);
                        Class valClass = v.getDataType().getValueClass();
//                        System.out.println("var " + v.getName() + " value " + value + " class " + valClass.getName());
                        factory.createValueExpression(context, "${" + v.getName() + "}", valClass).setValue(context, value);
                        found = true;
                    }
                }
            }

            factory.createValueExpression(context, "${chain}", Chain.class).setValue(context, (nodeContext instanceof Chain?nodeContext : nodeContext.getParentChain()));
        }

        //System.out.println("evaluating: " + valExpression.getExpressionString());
        try {
            return valExpression.getValue(context);
        } catch (NullPointerException ex) {
            throw new IllegalArgumentException("While evaluating " + valExpression.getExpressionString() + ": A method was not found.");
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new IllegalArgumentException("While evaluating " + valExpression.getExpressionString() + ": " + ex);
        }
    }

/*    private String parseNotes(String expr) throws IllegalArgumentException {
        StringBuffer b = new StringBuffer();
        Matcher m = notePattern.matcher(expr);
        while (m.find()) {
            m.appendReplacement(b, ""+MidiUtility.stringToNoteNumber(expr.substring(m.start()+1, m.end()-1)));
        }
        m.appendTail(b);
        return b.toString();
    }

    private String parseUnits(String expr) throws IllegalArgumentException {
        StringBuffer b = new StringBuffer();
        Matcher m = unitsPattern.matcher(expr);
        while (m.find()) {
            m.appendReplacement(b, ""+UnitParser.parse(expr.substring(m.start()+1, m.end()-1)));
        }
        m.appendTail(b);
        return b.toString();
    }*/

    @Override
    public String toString() {
        String expr = valExpression.getExpressionString();

        if (requireApostrophes && !(expr.startsWith("${") && Tokenizer.balanced(expr, 1, '{', '}')==expr.length()-1))
            return "'" + expr + "'";
        return expr;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof dcExpression) {
            if (super.equals(obj)) return true;
            else {
                dcExpression that = (dcExpression)obj;
                return (that.valExpression.equals(this.valExpression) && that.returnType==this.returnType);
            }
        } else return false;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 79 * hash + (this.returnType != null ? this.returnType.hashCode() : 0);
        hash = 79 * hash + (this.valExpression != null ? this.valExpression.hashCode() : 0);
        return hash;
    }

}

